1 module hip.graphics.g2d.animation;
2 
3 import hip.util.reflection : ExportD;
4 import hip.error.handler;
5 import hip.assets.textureatlas;
6 
7 import hip.api.graphics.color;
8 import hip.api.renderer.texture : IHipTextureRegion;
9 public import hip.api.graphics.g2d.animation;
10 
11 /**
12 *   This class uses multiplication for selecting the current frame, so, depending on the frame rate, it can cause
13 *   frame skipping, for giving better freedom for speeding up animation
14 */
15 @ExportD class HipAnimationTrack : IHipAnimationTrack
16 {
17     private immutable string _name;
18 
19     protected HipAnimationFrame[] frames;
20     protected float accumulator = 0;
21 
22     ///Internal state management
23     protected uint framesPerSecond = 0;
24     protected uint currentFrame = 0;
25 
26     ///Micro optimization for not doing frames.length - 1 every time
27     private uint lastFrame = 0;
28 
29     //Those three are a question if they should be in the track or in the animation controller
30     protected bool isPlaying = false;
31     protected bool isAdvancingForward = true;
32     protected HipAnimationLoopingMode _loopingMode;
33     protected bool _reverse  = false;
34 
35     this(string trackName, uint framesPerSecond, HipAnimationLoopingMode loopingMode = HipAnimationLoopingMode.none)
36     {
37         this._name = trackName;
38         setFramesPerSecond(framesPerSecond);
39         _loopingMode = loopingMode;
40     }
41     string name() const => _name;
42     HipAnimationLoopingMode loopingMode() const =>  _loopingMode;
43     HipAnimationLoopingMode loopingMode(HipAnimationLoopingMode loopingMode = HipAnimationLoopingMode.reset) => _loopingMode = loopingMode;
44     bool reverse() const => _reverse;
45     bool reverse(bool setReverse) => _reverse = setReverse;
46     float getDuration() const => cast(float)frames.length / framesPerSecond;
47 
48     /**
49     *   Use this version if you wish a more custom frame
50     */
51     IHipAnimationTrack addFrames(HipAnimationFrame[] frame...)
52     {
53         foreach(f; frame)
54             frames~= f;
55         if(frames.length > 0)
56             lastFrame = cast(typeof(lastFrame))frames.length - 1;
57         return this;
58     }
59 
60     IHipAnimationTrack addFrames(IHipTextureRegion[] regions...)
61     {
62         foreach(r; regions)
63             frames~= HipAnimationFrame(r);
64         if(frames.length > 0)
65             lastFrame = cast(typeof(lastFrame))frames.length - 1;
66         return this;
67     }
68     void reset(){currentFrame = 0;accumulator = 0;}
69     void setFrame(uint frame)
70     {
71         version(HipOptimize){}
72         else
73             ErrorHandler.assertLazyExit(frame < frames.length, "Frame is out of bounds on track "~name);
74         accumulator = frame*(1.0f/framesPerSecond);
75         currentFrame = frame;
76     }
77     void setFramesPerSecond(uint fps){framesPerSecond = fps;}
78 
79     HipAnimationFrame* getFrameForTime(float time)
80     {
81         uint frame = (cast(uint)time*framesPerSecond);
82         if(frame > lastFrame)
83             frame = lastFrame;
84         if(_reverse)
85             frame = lastFrame - frame;
86         return &frames[frame];
87     }
88 
89     HipAnimationFrame* getFrameForProgress(float progress)
90     {
91         uint frame = cast(uint)(progress*frames.length);
92         if(frame > lastFrame)
93             frame = lastFrame;
94         if(_reverse)
95             frame = lastFrame - frame;
96         return &frames[frame];
97     }
98 
99     HipAnimationFrame* update(float dt)
100     {
101         if(frames.length == 0)
102             return null;
103         accumulator+= dt;
104         uint frame = cast(uint)(accumulator*framesPerSecond);
105         if(frame > lastFrame)
106         {
107             final switch(_loopingMode) with(HipAnimationLoopingMode)
108             {
109                 case reset:
110                     accumulator = 0;
111                     frame = 0;
112                     break;
113                 case pingpong:
114                     frame = 0;
115                     accumulator = 0;
116                     isAdvancingForward = !isAdvancingForward;
117                     break;
118                 case none:
119                     accumulator-=dt;
120                     frame = lastFrame;
121                     break;
122             }
123         }
124         if((_loopingMode == HipAnimationLoopingMode.pingpong && !isAdvancingForward) || 
125         (_reverse && _loopingMode != HipAnimationLoopingMode.pingpong))
126             frame = lastFrame - frame;
127         currentFrame = frame;
128         return &frames[frame];
129     }
130     
131     
132 }
133 
134 /**
135 *   Currently used as a wrapper for holding animation tracks. Could probably do
136 *   advanced work as setting track markers for playing tracks sequentially. Setting general animation
137 *   speed 
138 */
139 @ExportD class HipAnimation : IHipAnimation
140 {
141     protected IHipAnimationTrack[string] tracks;
142     immutable string name;
143     protected float timeScale;
144     protected IHipAnimationTrack currentTrack;
145     protected HipAnimationFrame* currentFrame;
146 
147 
148     this(string name)
149     {
150         this.name = name;
151         this.timeScale = 1.0f;
152     }
153 
154     static HipAnimation fromAtlas(HipTextureAtlas atlas, string which, uint fps, HipAnimationLoopingMode loopingMode = HipAnimationLoopingMode.none)
155     {
156         import hip.util.conv:to;
157         HipAnimation ret = new HipAnimation(which);
158         HipAnimationTrack track = new HipAnimationTrack(which, fps, loopingMode);
159         AtlasFrame* frame;
160         int i = 1;
161         while((frame = (which~"_"~to!string(i) in atlas)) != null)
162         {
163             track.addFrames(HipAnimationFrame(frame.region));
164             i++;
165         }
166         ret.addTrack(track);
167 
168         return ret;
169     }
170 
171     IHipAnimation addTrack(IHipAnimationTrack track)
172     {
173         if(currentTrack is null)
174         {
175             currentTrack = track;
176             update(0);//Updates the current frame
177         }
178         ErrorHandler.assertExit((track.name in tracks) == null,
179         "Track named "~track.name~" is already on animation '"~name~"'");
180         tracks[track.name] = track;
181         return this;
182     }
183     IHipAnimationTrack getCurrentTrack() {return currentTrack;}
184     HipAnimationFrame* getCurrentFrame() {return currentFrame;}
185     void setTimeScale(float scale){timeScale = scale;}
186     void play(string trackName)
187     {
188         IHipAnimationTrack* track = trackName in tracks;
189         version(HipOptimize){}
190         else
191             ErrorHandler.assertLazyExit(track != null,
192                 "Track "~trackName~" does not exists in the animation '"~name~"'.");
193 
194         if(currentTrack !is null)
195             currentTrack.reset();
196         currentTrack = *track;
197         update(0); //Updates the current frame
198     }
199 
200     IHipAnimationTrack getTrack(string trackName)
201     {
202         IHipAnimationTrack* track = trackName in tracks;
203         if(track is null) return null;
204         return *track;
205     }
206 
207 
208     void update(float dt)
209     {
210         if(currentTrack is null)
211             return;
212         currentFrame = currentTrack.update(dt*timeScale);
213     }
214 }